package cn.matrix42.potoo.format.json.parser;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;

import cn.matrix42.potoo.annotation.Public;
import cn.matrix42.potoo.format.common.Parser;

@Public
public class JsonParser implements Parser {

    private int cur;
    private Character[] tokens;

    @Override
    public String getParserType() {
        return "json";
    }

    public JsonObject parseJson(String json) {
        this.tokens = toCharArrayWithSkipWhitespaceChar(json);
        this.cur = 0;
        return parseJsonObject();
    }

    /**
     * 2个引号之间的空白字符不移除
     * 其余都可以移除
     * @param json json
     * @return Character[]
     */
    private Character[] toCharArrayWithSkipWhitespaceChar(String json) {
        List<Character> chars = new ArrayList<>();
        int quotCount = 0;
        boolean allowRemoveWhitespace = true;
        for (char ch : json.toCharArray()) {
            if (Token.QUOTE.equals(ch)) {
                quotCount++;
                allowRemoveWhitespace = quotCount % 2 != 1;
            }
            if (!allowRemoveWhitespace || !isWhitespaceChar(ch)) {
                chars.add(ch);
            }
        }
        return chars.toArray(new Character[0]);
    }

    private boolean isWhitespaceChar(char ch) {
        switch (ch) {
            case ' ':
            case '\t':
            case '\r':
            case '\n':
                return true;
            default:
                return false;
        }
    }

    private void read(Character character) {
        char next = tokens[cur];
        if (character.equals(next)) {
            cur++;
        } else {
            throw new IllegalStateException(String.format("expect %s but got %s.", character, next));
        }
    }

    private boolean readIf(Character character) {
        char next = tokens[cur];
        if (character.equals(next)) {
            cur++;
            return true;
        }
        return false;
    }

    private Optional<String> readOptionalKey() {
        StringBuilder sb = new StringBuilder();
        if (Token.QUOTE.equals(tokens[cur])) {
            cur++;
            while(!Token.QUOTE.equals(tokens[cur])) {
                char next = tokens[cur];
                sb.append(next);
                cur++;
            }
            read(Token.QUOTE);
            return Optional.of(sb.toString());
        }
        return Optional.empty();
    }

    private ObjectType predictNextValueType() {
        if (Token.OPEN_BRACKET.equals(tokens[cur])) {
            return ObjectType.JSON_OBJECT;
        } else if (Token.OPEN_BRACE.equals(tokens[cur])) {
            return ObjectType.JSON_ARRAY;
        } else {
            return ObjectType.JSON_LITERAL;
        }
    }

    private JsonNode readValue() {
        ObjectType objectType = predictNextValueType();
        switch(objectType) {
            case JSON_OBJECT: {
                return parseJsonObject();
            }
            case JSON_ARRAY: {
                return parseJsonArray();
            }
            case JSON_LITERAL: {
                return parseJsonLiteral();
            }
        }
        throw new IllegalArgumentException();
    }

    private JsonObject parseJsonObject() {
        read(Token.OPEN_BRACKET);
        JsonObject jsonObject = new JsonObject();
        Optional<String> optionalKey;
        do {
            optionalKey = readOptionalKey();
            if (optionalKey.isPresent()) {
                String key = optionalKey.get();
                read(Token.COLON);
                JsonNode node = readValue();
                if (node.isJsonObject()) {
                    jsonObject.put(key, (JsonObject) node);
                } else if (node.isJsonArray()) {
                    jsonObject.put(key, (JsonArray) node);
                } else if (node.isJsonLiteral()) {
                    jsonObject.put(key, (JsonLiteral) node);
                }
            }
            readIf(Token.COMMA);
        } while(optionalKey.isPresent());
        read(Token.CLOSE_BRACKET);
        return jsonObject;
    }

    private JsonArray parseJsonArray() {
        read(Token.OPEN_BRACE);
        JsonArray jsonArray = new JsonArray();
        JsonNode node = readValue();


        read(Token.CLOSE_BRACE);
        return jsonArray;
    }

    private JsonLiteral parseJsonLiteral() {
        StringBuilder sb = new StringBuilder();
        boolean isQuote = readIf(Token.QUOTE);
        char next = tokens[cur];
        while (!isEndChar(next)) {
            sb.append(next);
            cur++;
            next = tokens[cur];
        }
        String literal = sb.toString();
        if (isQuote) {
            read(Token.QUOTE);
            return JsonLiteral.of(literal);
        }

        JsonLiteral jsonLiteral;
        if (isBooleanLiteral(literal)) {
            jsonLiteral = JsonLiteral.of(Boolean.parseBoolean(literal));
        } else if (isIntLiteral(literal)) {
            jsonLiteral = JsonLiteral.of(Integer.parseInt(literal));
        }  else if (isDoubleLiteral(literal)) {
            jsonLiteral = JsonLiteral.of(Double.parseDouble(literal));
        }  else if (isNullLiteral(literal)) {
            jsonLiteral = JsonLiteral.ofNull();
        } else {
            throw new IllegalArgumentException();
        }
        return jsonLiteral;
    }

    private boolean isEndChar(char ch) {
        return Token.COMMA.equals(ch) ||
            Token.QUOTE.equals(ch) ||
            Token.CLOSE_BRACKET.equals(ch) ||
            Token.CLOSE_BRACE.equals(ch);
    }

    private boolean isBooleanLiteral(String str) {
        return "true".equals(str) || "false".equals(str);
    }

    private boolean isIntLiteral(String str) {
        if (str == null || str.isEmpty()) {
            return false;
        }
        Pattern pattern = Pattern.compile("^[-\\+]?[\\d]*$");
        return pattern.matcher(str).matches();
    }

    private boolean isNullLiteral(String str) {
        if (str == null || str.isEmpty()) {
            return false;
        }
        return "null".equals(str);
    }

    private boolean isDoubleLiteral(String str) {
        if (str == null || str.isEmpty()) {
            return false;
        }
        Pattern pattern = Pattern.compile("[+-]?[0-9]+(\\.[0-9]{1,4})?");
        return pattern.matcher(str).matches();
    }

}
